class ShaderProgram {
    constructor(holder, options = {}) {
        options = Object.assign(
            {
                antialias: false,
                depthTest: false,
                mousemove: false,
                autosize: true,
                side: "front",
                vertex: `
precision highp float;

attribute vec4 a_position;
attribute vec4 a_color;

uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mousemove;
uniform mat4 u_projection;

varying vec4 v_color;

void main() {

  gl_Position = u_projection * a_position;
  gl_PointSize = (10.0 / gl_Position.w) * 100.0;

  v_color = a_color;

}`,
                fragment: `
precision highp float;

uniform sampler2D u_texture;
uniform int u_hasTexture;

varying vec4 v_color;

void main() {

  if ( u_hasTexture == 1 ) {

    gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord);

  } else {

    gl_FragColor = v_color;

  }

}`,
                uniforms: {},
                buffers: {},
                camera: {},
                texture: null,
                onUpdate: () => {},
                onResize: () => {},
            },
            options
        );

        const uniforms = Object.assign(
            {
                time: { type: "float", value: 0 },
                hasTexture: { type: "int", value: 0 },
                resolution: { type: "vec2", value: [0, 0] },
                mousemove: { type: "vec2", value: [0, 0] },
                projection: {
                    type: "mat4",
                    value: [
                        1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
                    ],
                },
            },
            options.uniforms
        );

        const buffers = Object.assign(
            {
                position: { size: 3, data: [] },
                color: { size: 4, data: [] },
            },
            options.buffers
        );

        const camera = Object.assign(
            {
                fov: 60,
                near: 1,
                far: 10000,
                aspect: 1,
                z: 100,
                perspective: true,
            },
            options.camera
        );

        const canvas = document.createElement("canvas");
        const gl = canvas.getContext("webgl", {
            antialias: options.antialias,
        });

        if (!gl) return false;

        this.count = 0;
        this.gl = gl;
        this.canvas = canvas;
        this.camera = camera;
        this.holder = holder;
        this.onUpdate = options.onUpdate;
        this.onResize = options.onResize;
        this.data = {};

        holder.appendChild(canvas);

        this.createProgram(options.vertex, options.fragment);

        this.createBuffers(buffers);
        this.createUniforms(uniforms);

        this.updateBuffers();
        this.updateUniforms();

        this.createTexture(options.texture);

        gl.enable(gl.BLEND);
        gl.enable(gl.CULL_FACE);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
        gl[options.depthTest ? "enable" : "disable"](gl.DEPTH_TEST);

        if (options.autosize)
            window.addEventListener(
                "resize",
                (e) => this.resize(e),
                false
            );
        if (options.mousemove)
            window.addEventListener(
                "mousemove",
                (e) => this.mousemove(e),
                false
            );

        this.resize();

        this.update = this.update.bind(this);
        this.time = {
            start: performance.now(),
            old: performance.now(),
        };
        this.update();
    }

    mousemove(e) {
        let x = (e.pageX / this.width) * 2 - 1;
        let y = (e.pageY / this.height) * 2 - 1;

        this.uniforms.mousemove = [x, y];
    }

    resize(e) {
        const holder = this.holder;
        const canvas = this.canvas;
        const gl = this.gl;

        const width = (this.width = holder.offsetWidth);
        const height = (this.height = holder.offsetHeight);
        const aspect = (this.aspect = width / height);
        const dpi = (this.dpi = devicePixelRatio);

        canvas.width = width * dpi;
        canvas.height = height * dpi;
        canvas.style.width = width + "px";
        canvas.style.height = height + "px";
        canvas.style.position = "fixed";
        canvas.style.bottom = "0";
        canvas.style.zIndex = "1";

        gl.viewport(0, 0, width * dpi, height * dpi);
        gl.clearColor(0, 0, 0, 0);

        this.uniforms.resolution = [width, height];
        this.uniforms.projection = this.setProjection(aspect);

        this.onResize(width, height, dpi);
    }

    setProjection(aspect) {
        const camera = this.camera;

        if (camera.perspective) {
            camera.aspect = aspect;

            const fovRad = camera.fov * (Math.PI / 180);
            const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad);
            const rangeInv = 1.0 / (camera.near - camera.far);

            const matrix = [
                f / camera.aspect,
                0,
                0,
                0,
                0,
                f,
                0,
                0,
                0,
                0,
                (camera.near + camera.far) * rangeInv,
                -1,
                0,
                0,
                camera.near * camera.far * rangeInv * 2,
                0,
            ];

            matrix[14] += camera.z;
            matrix[15] += camera.z;

            return matrix;
        } else {
            return [
                2 / this.width,
                0,
                0,
                0,
                0,
                -2 / this.height,
                0,
                0,
                0,
                0,
                1,
                0,
                -1,
                1,
                0,
                1,
            ];
        }
    }

    createShader(type, source) {
        const gl = this.gl;
        const shader = gl.createShader(type);

        gl.shaderSource(shader, source);
        gl.compileShader(shader);

        if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            return shader;
        } else {
            gl.deleteShader(shader);
        }
    }

    createProgram(vertex, fragment) {
        const gl = this.gl;

        const vertexShader = this.createShader(
            gl.VERTEX_SHADER,
            vertex
        );
        const fragmentShader = this.createShader(
            gl.FRAGMENT_SHADER,
            fragment
        );

        const program = gl.createProgram();

        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);

        if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
            gl.useProgram(program);
            this.program = program;
        } else {
            gl.deleteProgram(program);
        }
    }

    createUniforms(data) {
        const gl = this.gl;
        const uniforms = (this.data.uniforms = data);
        const values = (this.uniforms = {});

        Object.keys(uniforms).forEach((name) => {
            const uniform = uniforms[name];

            uniform.location = gl.getUniformLocation(
                this.program,
                "u_" + name
            );

            Object.defineProperty(values, name, {
                set: (value) => {
                    uniforms[name].value = value;
                    this.setUniform(name, value);
                },
                get: () => uniforms[name].value,
            });
        });
    }

    setUniform(name, value) {
        const gl = this.gl;
        const uniform = this.data.uniforms[name];

        uniform.value = value;

        switch (uniform.type) {
            case "int": {
                gl.uniform1i(uniform.location, value);
                break;
            }
            case "float": {
                gl.uniform1f(uniform.location, value);
                break;
            }
            case "vec2": {
                gl.uniform2f(uniform.location, ...value);
                break;
            }
            case "vec3": {
                gl.uniform3f(uniform.location, ...value);
                break;
            }
            case "vec4": {
                gl.uniform4f(uniform.location, ...value);
                break;
            }
            case "mat2": {
                gl.uniformMatrix2fv(uniform.location, false, value);
                break;
            }
            case "mat3": {
                gl.uniformMatrix3fv(uniform.location, false, value);
                break;
            }
            case "mat4": {
                gl.uniformMatrix4fv(uniform.location, false, value);
                break;
            }
        }

        // ivec2       : uniform2i,
        // ivec3       : uniform3i,
        // ivec4       : uniform4i,
        // sampler2D   : uniform1i,
        // samplerCube : uniform1i,
        // bool        : uniform1i,
        // bvec2       : uniform2i,
        // bvec3       : uniform3i,
        // bvec4       : uniform4i,
    }

    updateUniforms() {
        const gl = this.gl;
        const uniforms = this.data.uniforms;

        Object.keys(uniforms).forEach((name) => {
            const uniform = uniforms[name];

            this.uniforms[name] = uniform.value;
        });
    }

    createBuffers(data) {
        const gl = this.gl;
        const buffers = (this.data.buffers = data);
        const values = (this.buffers = {});

        Object.keys(buffers).forEach((name) => {
            const buffer = buffers[name];

            buffer.buffer = this.createBuffer("a_" + name, buffer.size);

            Object.defineProperty(values, name, {
                set: (data) => {
                    buffers[name].data = data;
                    this.setBuffer(name, data);

                    if (name == "position")
                        this.count = buffers.position.data.length / 3;
                },
                get: () => buffers[name].data,
            });
        });
    }

    createBuffer(name, size) {
        const gl = this.gl;
        const program = this.program;

        const index = gl.getAttribLocation(program, name);
        const buffer = gl.createBuffer();

        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.enableVertexAttribArray(index);
        gl.vertexAttribPointer(index, size, gl.FLOAT, false, 0, 0);

        return buffer;
    }

    setBuffer(name, data) {
        const gl = this.gl;
        const buffers = this.data.buffers;

        if (name == null && !gl.bindBuffer(gl.ARRAY_BUFFER, null))
            return;

        gl.bindBuffer(gl.ARRAY_BUFFER, buffers[name].buffer);
        gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array(data),
            gl.STATIC_DRAW
        );
    }

    updateBuffers() {
        const gl = this.gl;
        const buffers = this.buffers;

        Object.keys(buffers).forEach(
            (name) => (buffers[name] = buffer.data)
        );

        this.setBuffer(null);
    }

    createTexture(src) {
        const gl = this.gl;
        const texture = gl.createTexture();

        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            gl.RGBA,
            1,
            1,
            0,
            gl.RGBA,
            gl.UNSIGNED_BYTE,
            new Uint8Array([0, 0, 0, 0])
        );

        this.texture = texture;

        if (src) {
            this.uniforms.hasTexture = 1;
            this.loadTexture(src);
        }
    }

    loadTexture(src) {
        const gl = this.gl;
        const texture = this.texture;

        const textureImage = new Image();

        textureImage.onload = () => {
            gl.bindTexture(gl.TEXTURE_2D, texture);

            gl.texImage2D(
                gl.TEXTURE_2D,
                0,
                gl.RGBA,
                gl.RGBA,
                gl.UNSIGNED_BYTE,
                textureImage
            );

            gl.texParameteri(
                gl.TEXTURE_2D,
                gl.TEXTURE_MIN_FILTER,
                gl.LINEAR
            );
            gl.texParameteri(
                gl.TEXTURE_2D,
                gl.TEXTURE_MAG_FILTER,
                gl.LINEAR
            );

            gl.texParameteri(
                gl.TEXTURE_2D,
                gl.TEXTURE_WRAP_S,
                gl.CLAMP_TO_EDGE
            );
            gl.texParameteri(
                gl.TEXTURE_2D,
                gl.TEXTURE_WRAP_T,
                gl.CLAMP_TO_EDGE
            );

            // gl.generateMipmap( gl.TEXTURE_2D )
        };

        textureImage.src = src;
    }

    update() {
        const gl = this.gl;

        const now = performance.now();
        const elapsed = (now - this.time.start) / 5000;
        const delta = now - this.time.old;
        this.time.old = now;

        this.uniforms.time = elapsed;

        if (this.count > 0) {
            gl.clear(gl.COLORBUFFERBIT);
            gl.drawArrays(gl.POINTS, 0, this.count);
        }

        this.onUpdate(delta);

        requestAnimationFrame(this.update);
    }
}
function createWaves(selector,pointSize){
    if(!pointSize){
        pointSize=3;
    }
    new ShaderProgram(document.querySelector(selector), {
    texture:
        "",
    uniforms: {
        size: { type: "float", value: pointSize },
        field: { type: "vec3", value: [0, 0, 0] },
        speed: { type: "float", value: 5 },
    },
    vertex: `
#define M_PI 3.1415926535897932384626433832795

precision highp float;

attribute vec4 a_position;
attribute vec4 a_color;

uniform float u_time;
uniform float u_size;
uniform float u_speed;
uniform vec3 u_field;
uniform mat4 u_projection;

varying vec4 v_color;

void main() {

vec3 pos = a_position.xyz;

pos.y += (
cos(pos.x / u_field.x * M_PI * 8.0 + u_time * u_speed) +
sin(pos.z / u_field.z * M_PI * 8.0 + u_time * u_speed)
) * u_field.y;

gl_Position = u_projection * vec4( pos.xyz, a_position.w );
gl_PointSize = ( u_size / gl_Position.w ) * 100.0;

v_color = a_color;

}`,
    fragment: `
precision highp float;

uniform sampler2D u_texture;

varying vec4 v_color;

void main() {

gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord);

}`,
    onResize(w, h, dpi) {
        const position = [],
            color = [];

        const width = 400 * (w / h);
        const depth = 400;
        const height = 3;
        const distance = 5;

        for (let x = 0; x < width; x += distance) {
            for (let z = 0; z < depth; z += distance) {
                position.push(-width / 2 + x, -30, -depth / 2 + z);
                color.push(
                    0,
                    1 - (x / width) * 1,
                    0.5 + (x / width) * 0.5,
                    z / depth
                );
            }
        }

        this.uniforms.field = [width, height, depth];

        this.buffers.position = position;
        this.buffers.color = color;

        this.uniforms.size = (h / 400) * pointSize * dpi;
    },
});
}
export default  createWaves;